Storage Transfer ServiceがS3へアクセスするときのIP範囲の変更検知方法を考えてみた

Storage Transfer ServiceがS3へアクセスするときのIP範囲の変更検知方法を考えてみた

Clock Icon2025.01.18

概要

Storage Transfer Service(以下STS)がAmazon S3にアクセスする際のアクセス元IPアドレスの範囲は2023/4/25より公開されています。
https://cloud.google.com/storage-transfer/docs/release-notes?hl=ja#April_25_2023

このIPアドレスの範囲をS3のバケットポリシーに設定することでSTSと連携するバケットにIP制限を設定することができます。
ただ、公開されているIPアドレスは変更される可能性があるとリファレンスに記載があります。

これらのIP範囲は変更される可能性があるため、永続的なアドレスに現在の値を JSON ファイルとして公開します。
https://www.gstatic.com/storage-transfer-service/ipranges.json
ファイルに新しい範囲が追加される場合、Storage Transfer Service からのリクエストに対してその範囲が使用されるまで少なくとも 7 日間は待機します。
セキュリティ構成を最新の状態に保つため、少なくとも週に 1 回、このドキュメントからデータを pull することをおすすめします。JSON ファイルから IP 範囲を取得する Python スクリプトの例については、Virtual Private Cloud のドキュメントをご覧ください。

引用:https://cloud.google.com/storage-transfer/docs/source-amazon-s3?hl=ja#ip_restrictions

ポイントは2点あると考えます。

  1. IP範囲はこちらでJSONファイルとして公開されています
  2. IP範囲が追加された場合、STSがS3へのリクエストに追加されたIP範囲を使用するまで少なくとも7日間ある

つまりは、公開されているIP範囲のJSONを取得して設定済みのIP範囲と差分があるかどうかチェックする仕組みを実装すればIP範囲の追加を見逃さずに済むということだと考えます。
(理想はIP範囲が追加されたらバケットポリシーに追加するという仕組みかもですが)
ただ今のところ2023/4/25に公開された時からIP範囲は追加されていないようなのでとりあえずはチェックする仕組みだけ実装してみようかなと思いました。
※いつ追加されるかはわからないので当然注意が必要です。

というわけで公開されているIP範囲のJSONを取得して、差分チェックして差分を検知する仕組みを考えてみました。よかったら読んでみてください。

やってみる

検知する仕組みのイメージは以下となります。
スクリーンショット 2025-01-18 0.44.15.png

  1. Cloud Run FunctionsでIP範囲のJSONを取得
  2. 初回実行ならFirestoreにそのまま保存
  3. 初回以外ならFirestoreのIP範囲とJSONで取得したIP範囲を比較
  4. 追加・削除があればログ出力(通知)

という流れです。これをCloud Run Functionsで実装してみました。
※Firestoreは設定済みの前提です。

とりあえずスクリプト全文です

main.py
import functions_framework
import json
import requests
from google.cloud import firestore
from datetime import datetime

# Firestoreコレクションとドキュメントの設定
COLLECTION_NAME = 'ip_ranges'
DOCUMENT_NAME = 'current'

# STSのIP範囲公開JSON URL
GCS_IP_RANGES_URL = 'https://www.gstatic.com/storage-transfer-service/ipranges.json'

def filter_data(data):
    """比較対象のデータから不要なフィールドを除外"""
    if data is None:
        return None
    return {
        "syncToken": data.get("syncToken"),
        "prefixes": data.get("prefixes")
    }

def find_differences(old_prefixes, new_prefixes):
    """新旧の prefixes を比較して差分を見つける"""
    old_set = set(json.dumps(prefix, sort_keys=True) for prefix in old_prefixes)
    new_set = set(json.dumps(prefix, sort_keys=True) for prefix in new_prefixes)

    added = new_set - old_set
    removed = old_set - new_set

    return {
        "added": [json.loads(item) for item in added],
        "removed": [json.loads(item) for item in removed]
    }

@functions_framework.http
def detect_gcs_ip_changes(request):
    try:
        # STSのIP範囲取得
        response = requests.get(GCS_IP_RANGES_URL)
        response.raise_for_status()
        new_ip_ranges = response.json()

        db = firestore.Client()
        doc_ref = db.collection(COLLECTION_NAME).document(DOCUMENT_NAME)

        # Firestoreから設定済みIP範囲取得
        doc_snapshot = doc_ref.get()
        if doc_snapshot.exists:
            old_ip_ranges = doc_snapshot.to_dict()
        else:
            old_ip_ranges = None

        # 差分があるかどうかをチェック
        if old_ip_ranges is None:
            print('No existing IP ranges found in Firestore. Saving the initial data.')

            # 初回データ保存
            new_ip_ranges['lastUpdated'] = datetime.utcnow().isoformat() + 'Z'
            doc_ref.set(new_ip_ranges)
            return 'Initial IP ranges saved.', 200

        # 差分比較
        differences = find_differences(
            old_prefixes=filter_data(old_ip_ranges).get("prefixes", []),
            new_prefixes=filter_data(new_ip_ranges).get("prefixes", [])
        )

        added = differences["added"]
        removed = differences["removed"]

        if added or removed:
            print('IP ranges have changed.')
            print(f'Added IPs: {json.dumps(added, indent=2)}')
            print(f'Removed IPs: {json.dumps(removed, indent=2)}')

            # Firestoreは更新せずログ出力のみ実施
            return 'IP ranges have changed. Check logs for details.', 200
        else:
            print('No changes in IP ranges.')
            return 'No changes in IP ranges.', 200

    except requests.exceptions.RequestException as e:
        print(f'Error fetching IP ranges: {e}')
        return 'Error fetching IP ranges.', 500

    except Exception as e:
        print(f'Error: {e}')
        return 'Error detecting IP changes.', 500
requirements.txt
functions-framework==3.*
requests
google-cloud-firestore

詳細な解説はしませんが、スクリプトの流れだけ説明します。

  1. 公開されているSTSのIP範囲を取得
  2. Firestoreからデータ取得
  3. Firestoreに保存されたIP範囲を取得
  4. Firestoreにデータが存在しない場合、取得したデータを保存(初回実行時)
  5. 差分の比較。新旧の prefixes を比較して差分を検出
    差分がない場合:「変更なし」として処理を終了
    差分がある場合:変更内容をログ出力して終了

実行してみる(初回実行)

作成したCloud Run関数を叩きます。私は認証ありにしたのでAuthorizationヘッダーにbearerトークンを設定しています。

curl -m 70 -X POST "Cloud Run関数のURL" -H "Authorization: bearer $(gcloud auth print-identity-token)"

叩くと以下のレスポンスが返ってきます。

Initial IP ranges saved.

初回実行なのでIP範囲がFirestoreに保存されました。
Firestoreも確認してみます。
スクリーンショット 2025-01-18 1.26.54.png
公開されている通りの値がドキュメントに設定されていることが確認できました。

続いて変更を検知できるか試してみます。

変更検知テスト(追加)

初回実行でドキュメントが作成されて2つのIP範囲が追加されています。このどちらか片方を削除してCloud Run関数を実行すれば、IP範囲が追加されたことと同義になります。
なので片方消して、Cloud Run関数を叩きます。
Firestoreのコンソールの消したい方のフィールドの右端にゴミ箱マークがあるのでそこから消せます。
スクリーンショット 2025-01-18 1.30.36.png

削除したら叩きます。
そうすると今度は出力が少し変わります。

IP ranges have changed. Check logs for details.

IP範囲が変更されたことを検知できているのでログを見てみます。
スクリーンショット 2025-01-18 1.33.21.png
削除した方のIP範囲がAdded IPsとして出力されていました。ちゃんと追加を検知できていますね。

変更検知テスト(削除)

リファレンスでは追加しかないようですが、一応削除された場合も検知できるか確かめてみます。
削除をテストするには、公開されているIP範囲に存在しないIP範囲をFirestoreのドキュメントに設定すればよいです。

prefixesの右側の+(プラス)ボタンを押下します。
スクリーンショット 2025-01-18 1.37.29.png

フィールドを追加でmapを選択して、フィールド名にipv4Prefix・フィールドタイプstring・フィールド値192.168.0.0/32を設定します。
スクリーンショット 2025-01-18 1.37.35.png

設定したらCloud Run関数を叩きます。叩いたらログをみます。
スクリーンショット 2025-01-18 1.40.43.png
Removed IPsに追加したIP範囲が出力されていることが確認できました。問題なく削除も検知できていますね。

無事意図した動作をしていてよかったです。

所感

この仕組みの使い道としては、出力ログを元にアラートを飛ばしたり、SlackやTeamsに通知をしてIP範囲の変更検知に繋げることができると思います。
ただ、あくまで検知だけなので実際に検知した場合にはS3のバケットポリシーのIP範囲を修正する必要があります。少なくとも 7 日間は待機します。とリファレンスにあるので、その間に対応しなければならないです。
つまりは運用体制も整備しておいた方が良さそうです。
IP範囲が今後追加されるかはわかりませんが、リファレンスでもPULLしてチェックしなさいという旨の記載があるとおりこのようなチェックの仕組みを持っておいた方が安心だなと思います。

それではまた。ナマステー

参考

https://cloud.google.com/storage-transfer/docs/source-amazon-s3?hl=ja#ip_restrictions

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.